# -*- coding:utf-8 -*-

import wolfox.lib.codeblock import function2block
import wolfox.foxit.utils.vdict as vdict


CTYPE = {'ONE':1,'ALL':2,'CATALOG':2}    #normal为与具体stock数据无关的计算

CALCED,GLOBAL,ALL,ALLCATALOG = 'CACLED','GLOBAL','ALL','ALLCATALOG'


class Context(object):
    ''' Task内部context组织
            data
            meta_data: catalogsubject_name ==> catalogs的dict
                       每个catalog是一个(catalogname,set)对
                       需要虚拟出一个 ALL ==>{'ALL':set1,'ALLCATALOG':set2}
            temp 以values或'GLOBAL'为key的dict. GLOBAL是指与values无关的函数计算所得的结果(如catalog指数，stockcode的全局排序号)
                每个值元素为stock_code或'CAT%d' % cat_id或'ALL'或'CALCED'为key的dict. 
                对于从CATALOG/ALL类型计算函数得到的registered结果,如果为二维数组则分解到stock_code列,一维则直接挂到'CAT%d'/all下
                    其中的每个值元素的结构为变量名为key的dict,如key为'ma5','order'之类的
                        对于直接的索引,获得的即为向量或相应数据
                        对于由CATALOG/ALL分解到stock的
                            如果是CATALOG分解来的,得到的是一个列表,其中的每个元素是(catalog_name,catalog自身的排名,data)
                            如果是ALL/ALLCATALOG分解来的,直接得到向量数组
                对于CALCED,数据为name==>value的dict.
            从广义来说,stock_code,catalog_name,ALL,ALLCATALOG都是同种性质的,都代表着广义的股票名
                       而values和GLOBAL也是同种性质,都代表着与参数相关的设置
                       CALCED是另类的,处于values之下时,属于values相关的标量数据;处于GLOBAL之下时,属于values无关的标量数据
                            但是这里并没有刻意去区分这个标量数据是怎么计算出来的.但是理论上,标量数据不应该从向量得到
                       经过考虑,去掉CALCED相关的东西,如果需要,在配置文件设定args时来计算(去eval为string的arg) 
            要求外部在初始化时设定好data和meta_data
            特别是meta_data,需要设定好all和allcatalog这两个特殊的catalog
    '''

    def __init__(self,data,meta_data):
        self.data = data
        self.meta_data = meta_data
        self.temp = vdict()

    def get_stocks(self):
        return self.data.keys()

    def get_datas(self,stock_code,names):
        return self._match(self.data,names)

    def get_temp_datas(self,key,stock_code,names):
        vback = self.temp.setdefault(key,vdict()).setdefault(stock_code,vdict())
        return self._match(vback,names)

    def set_temp_datas(self,key,stock_code,pairs):
        vback = self.temp.setdefault(key,vdict()).setdefault(stock_code,vdict())
        vback.update(pairs)

    def get_catalogs(self,csname):
        ''' 返回 catalog_name,set(stock_code) 的元组Set '''
        return self.meta_data[csname].catalogs

    def get_subject_type(self,csname):
        return self.meta_data[csname].ctype


    #内部函数
    def _match(self,data,names):
        return dict([(name,data[name]) for name in names])

    #两个快捷方式
    def get_global_datas(self,stock_code,names):
        return self.get_temp_datas(GLOBAL,stock_code,names)

    def set_global_datas(self,stock_code,pairs):
        return self.set_temp_datas(GLOBAL,stock_code,pairs)

    #另两个常用的快捷方式
    def get_independent_datas(self,stock_code,names):
        vback = self.get_datas(stock_code,names)
        vback.update(self.get_global_datas(stock_code,names))
        return vback

    def get_independent_datas(self,args,stock_code,names):
        vback = self.get_independent_datas(stock_code,names)
        vback.update(self.get_temp_datas(args,stock_code,names))
        return vback


class Task(object):
    ''' 核心任务类

        负责接受script和相应的参数组
        给出验证script合法性的方法
        按照参数组和传入的背景数据对象(可能缓存)执行任务

          
         要求脚本中变量名不要和全局变量名和register变量名重复,否则结果难以预料
    '''

    header = '''
# -*- coding: utf-8 -*-

import re
import logging
from wolfox.foxit.calculator import *

logger = logging.getLogger('foxit.dune.controller')

'''

    def __init__(self,script,arg_fields,args_list,predefined):
        ''' script: 脚本对象，为string. 其中只允许一层
            arg_fields为自由变量名列表
            args_list 为变量值列表，其中的每个元素为与arg_fields对应的值元组
            predefined为name==>value的dict,为预定义的常量
        '''
        assert (not arg_fields and not args_list) or len(arg_fields) == len(args_list[0])   #如果存在自由变量，则必有args_list        
        self.script = script
        self.arg_fields = arg_fields
        self.args_list = [tuple(args) for args in args_list]    #元素转换为tuple以备用作dict的key
        self.predefined = predefined        
        self.cobject = compile(self.header + self.script,'<string>','exec')
        self.functions = []
        self.globals = self._prepare()  #脚本中的全局变量
        self.item_names = self._get_data_item_names()
        self.context = Context()

    def _prepare(self):
        ''' 执行脚本，过程中用到了calc这个decorator，因此又会更新self,functions
        '''
        g = {'calc':self.calc}
        l = {}
        exec self.cobject in g,l
        self.functions.sort()   #按order来
        return l

    def reset_values(self,args_list):
        ''' 重新设置args_list'''
        self.args_list = args_list

    def reset_predefined(self,predefined):  
        ''' 如默认指数,特定常量(如ctype类型 NORMAL,ALL,GROUP)等
        '''
        self.predefined = predefined

    def is_valid(self):
        ''' predefines为预定义的global变量集合
            验证每个calc function中用到的变量(co_names)是否有指定变量,输出变量是否是内部变量(co_varnames)
            可能会抛出NameError
        '''
        
        spre = set(predefined.keys)
        svar = set(self.arg_fields)
        sglobal = set(self.globals.keys)
        sglobal.update(spre)
        sglobal.update(svar)
        is_valid = True
        for nth,func in self.functions:
            snames = set(func.func_code.co_names)
            if not snames.issubset(sglobal):
                raise NameError,u'function %s 所用变量不存在' % func.func_code.co_name
            souts = set(func.register)
            if not souts.issubset(set(func.func_code.co_varnames)):
                raise NameError,u'function %s 输出的变量不存在' % func.func_code.co_name
            sglobal.update(souts)   #outs部分添加到全局变量集合
        return True

    def _get_data_item_names(self):
        ''' 计算实际用到的data item_names '''
        item_names = set([])
        svar = set(self.arg_fields)
        for nth,func in self.functions:
            item_names.update(svar.intersection(set(func.func_code.co_names)))
        return list(item_names)

    def run(self,context):
        ''' context:设置脚本执行globals的dict
            执行过程中的结果输出也在此步完成
            这里区分ALL和Catalog的原因是在后续处理上有所不同。虽然可以不区分，在run_group函数里面判断是哪个subject
            但是为使使用时有明确区别，用ALL/CATALOG来区分这些不同。
        '''
        sglobal = self.globals.copy()
        sglobals.update(self.predefined)
        for nth,func in self.functions:
            if func.ctype = CTYPE['ONE']:
                self.run_one(func,sglobals,conext)
            elif func_ctype = CTYPE['CATALOG']:
                self.run_group(func,sglobals,context,subjects)
            elif func_ctype = CTYPE['ALL']:
                self.run_all(func,sglobals,context)


    #decorator定义. 因为calc中要用到task的实例来保存script中的函数信息,但在脚本中无法隐式得到这个实例,因此直接定义在task中
    def calc(self,order,ctype=CTYPE['ONE'],register=(),subjects=(),once=False):
        ''' subjects只对ctype='CATALOG'才有意义 
            once表明该函数是否对arg_list有依赖.若为True,则只须计算一次
        '''
        def real_dec(f):
            f.once=once
            f.register = register
            f.ctype = ctype
            f.subjects = subjects
            function2block(f)
            self.functions.append(order,new_func)
            return f
        return real_dec

    def run_one(self,func,sglobals,context): 
        my_globals = sglobals.copy()
        gnames = func.func_code.co_names
        if func.once == True:
            for stock_code in context.get_stocks():
                my_globals.update(self.context.get_independent_datas(stock_code,gnames)) #每次都会覆盖之前的同名变量
                my_locals = {}
                exec func.func_code in my_globals,my_locals
                self.context.set_global_datas(stock_code,[(varname,my_locals[varname]) for varname in func_register])
        else:   #逐个执行. 对脚本的变量集合逐个执行每个股票
            for args in self.args_list:
                my_globals.update(zip(self.arg_fields,args))
                for stock_code in context.get_stocks():
                    my_globals.update(self.context.get_dependent_datas(args,stock_code,gnames))  #每次都会覆盖之前的同名变量
                    my_locals = {}
                    exec func.func_code in my_globals,my_locals
                    self.context.set_temp_datas(args,stock_code,[(varname,my_locals[varname]) for varname in func_register])


    def run_group(self,func,sglobals,context,subjects):  #逐组执行
        my_globals = sglobals.copy()
        gnames = func.func_code.co_names
        for subject in subjects:
            for name,catalog in context.get_catalogs(subject):
                if func.once == True:
                    pass
                else:
                    for args in self.args_list:
                        my_globals.update(zip(self.arg_fields,args))
            

    def run_all(self,func,sglobals,context):
        subject = 'ALL'
        my_globals = sglobals.copy()
        gnames = func.func_code.co_names
        for name,catalog in context.get_catalogs(subject):
            if func.once == True:
                pass
            else:
                for args in self.args_list:
                    my_globals.update(zip(self.arg_fields,args))

                    

